package com.hw.shortlink.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hw.shortlink.project.common.convention.exception.ServiceException;
import com.hw.shortlink.project.dao.entity.LinkAccessStats;
import com.hw.shortlink.project.dao.entity.ShortLink;
import com.hw.shortlink.project.dao.entity.ShortLinkGoto;
import com.hw.shortlink.project.dao.mapper.*;
import com.hw.shortlink.project.dto.mq.StatsMessageDto;
import com.hw.shortlink.project.dto.req.ShortLinkCreateBatchReqDto;
import com.hw.shortlink.project.dto.req.ShortLinkCreateReqDto;
import com.hw.shortlink.project.dto.req.ShortLinkPageReqDto;
import com.hw.shortlink.project.dto.req.ShortLinkUpdateReqDto;
import com.hw.shortlink.project.dto.resp.ShortLinkCreateBatchRespDto;
import com.hw.shortlink.project.dto.resp.ShortLinkCreateRespDto;
import com.hw.shortlink.project.dto.resp.ShortLinkGroupCountQueryRespDto;
import com.hw.shortlink.project.dto.resp.ShortLinkPageRespDto;
import com.hw.shortlink.project.mq.ShortLinkStatsProducer;
import com.hw.shortlink.project.service.ShortLinkService;
import com.hw.shortlink.project.service.URLInfoService;
import com.hw.shortlink.project.until.ClientMsg;
import com.hw.shortlink.project.until.LinkUtil;
import com.hw.shortlink.project.until.RandomGenerator;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import static com.hw.shortlink.project.common.constant.RedisKeyConstants.*;

@Service
@RequiredArgsConstructor
@Slf4j
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLink> implements ShortLinkService {


    private final RBloomFilter<String> shortLinkCreateCachePenetrationBloomFilter;
    private final ShortLinkGotoMapper shortLinkGotoMapper;
    private final StringRedisTemplate stringRedisTemplate;
    private final RedissonClient redissonClient;
    private final LinkAccessStatsMapper linkAccessStatsMapper;
    private final LinkLocaleStatsMapper linkLocaleStatsMapper;
    private final LinkOsStatsMapper linkOsStatsMapper;
    private final LinkBrowserStatsMapper linkBrowserStatsMapper;
    private final LinkAccessLogsMapper linkAccessLogsMapper;
    private final LinkDeviceStatsMapper linkDeviceStatsMapper;
    private final LinkNetworkStatsMapper linkNetworkStatsMapper;
    private final URLInfoService urlInfoService;
    private final ShortLinkStatsProducer shortLinkStatsProducer;


    @Value("${shortlink.locale.stats.key}")
    private String amapKey;

    @Value("${shortlink.domain.default}")
    private String createShortLinkDefaultDomain;


    @Override
    @Transactional(rollbackFor = Exception.class)
    public ShortLinkCreateRespDto createShortLink(ShortLinkCreateReqDto shortLinkCreateReqDTO) {


        ShortLink shortLink = BeanUtil.copyProperties(shortLinkCreateReqDTO, ShortLink.class);

        //设置默认可用的域名
        shortLink.setDomain(createShortLinkDefaultDomain);

        if (shortLink.getValidDateType() == 0) {//如果为永久的短链接将validDate设置为null
            shortLink.setValidDate(null);
        }
        //生成短链接
        String shortLinkSuffix = generateSuffix();


        //新的url
        String fullShortUrl = shortLink.getDomain() + "/" + shortLinkSuffix;

        shortLink.setFullShortUrl(fullShortUrl);
        shortLink.setShortUri(shortLinkSuffix);
        shortLink.setEnableStatus(0);
        shortLink.setFavicon(urlInfoService.getFavicon(shortLinkCreateReqDTO.getOriginUrl()));

        try {
            save(shortLink);
            //修改tb_link_goto 表
            ShortLinkGoto shortLinkGoto = new ShortLinkGoto();
            shortLinkGoto.setGid(shortLink.getGid());
            shortLinkGoto.setFullShortUrl(fullShortUrl);

            shortLinkGotoMapper.insert(shortLinkGoto);

        } catch (DuplicateKeyException e) {
            //防止对同一个短链接的创建多线程的问题
            log.warn("短链接{}重复入库 ", fullShortUrl);
            throw new ServiceException("短链接生成重复");
        }


        stringRedisTemplate.opsForValue()
                .set(REDIRECT_KEY + fullShortUrl, shortLink.getOriginUrl(),
                        LinkUtil.getLinkCacheValidTime(shortLink.getValidDate()),
                        TimeUnit.MILLISECONDS);
        //添加到布隆过滤器
        shortLinkCreateCachePenetrationBloomFilter.add(fullShortUrl);
        return ShortLinkCreateRespDto.builder()
                .gid(shortLinkCreateReqDTO.getGid())
                .originUrl(shortLinkCreateReqDTO.getOriginUrl())
                .fullShortUrl(fullShortUrl)
                .build();
    }


    @Override
    public ShortLinkCreateBatchRespDto createShortLinkBatch(ShortLinkCreateBatchReqDto shortLinkCreateBatchReqDto) {

        ShortLinkCreateReqDto shortLinkCreateReqDto = BeanUtil.copyProperties(shortLinkCreateBatchReqDto, ShortLinkCreateReqDto.class);
        int n = shortLinkCreateBatchReqDto.getOriginUrls().size();
        List<ShortLinkCreateRespDto> result = new ArrayList<>();
        for (int i = 0; i < n; i++) {

            shortLinkCreateReqDto.setOriginUrl(shortLinkCreateBatchReqDto.getOriginUrls().get(i));
            shortLinkCreateReqDto.setDescribe(shortLinkCreateBatchReqDto.getDescribes().get(i));

            //todo 防止事务失效
            ShortLinkCreateRespDto shortLink = createShortLink(shortLinkCreateReqDto);
            result.add(shortLink);

        }
        ShortLinkCreateBatchRespDto shortLinkCreateBatchRespDto = new ShortLinkCreateBatchRespDto();
        shortLinkCreateBatchRespDto.setTotal(result.size());
        shortLinkCreateBatchRespDto.setBaseLinkInfos(result);
        return shortLinkCreateBatchRespDto;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateShortLink(ShortLinkUpdateReqDto shortLinkUpdateReqDTO) {
        // 判断短链接是否存在
        ShortLink originLink = lambdaQuery()
                .eq(ShortLink::getDelFlag, 0)
                .eq(ShortLink::getEnableStatus, 0)
                .eq(ShortLink::getGid, shortLinkUpdateReqDTO.getOriginGid())
                .eq(ShortLink::getFullShortUrl, shortLinkUpdateReqDTO.getFullShortUrl())
                .one();
        if (originLink == null) {
            throw new ServiceException("短链接不存在");
        }


        if (Objects.equals(shortLinkUpdateReqDTO.getGid(), shortLinkUpdateReqDTO.getOriginGid())) {
            //没修改gid,直接更新
            BeanUtil.copyProperties(shortLinkUpdateReqDTO, originLink);
            lambdaUpdate().eq(ShortLink::getDelFlag, 0)
                    .eq(ShortLink::getEnableStatus, 0)
                    .eq(ShortLink::getGid, shortLinkUpdateReqDTO.getOriginGid())
                    .eq(ShortLink::getFullShortUrl, shortLinkUpdateReqDTO.getFullShortUrl())
                    .update(originLink);
        } else {
            //修改了gid,需要删除原来表中的记录，再插入

            //删除
            originLink.setDelFlag(1);
            originLink.setDelTime(new Date());
            lambdaUpdate().eq(ShortLink::getDelFlag, 0)
                    .eq(ShortLink::getEnableStatus, 0)
                    .eq(ShortLink::getGid, shortLinkUpdateReqDTO.getOriginGid())
                    .eq(ShortLink::getFullShortUrl, shortLinkUpdateReqDTO.getFullShortUrl())
                    .update(originLink);

            //插入
            originLink.setDelTime(null);
            originLink.setDelFlag(0);
            originLink.setId(null);
            BeanUtil.copyProperties(shortLinkUpdateReqDTO, originLink);
            save(originLink);

            //修改goto表
            ShortLinkGoto shortLinkGoto = new ShortLinkGoto();
            shortLinkGoto.setGid(shortLinkUpdateReqDTO.getGid());
            shortLinkGotoMapper.update(shortLinkGoto, new LambdaUpdateWrapper<ShortLinkGoto>()
                    .eq(ShortLinkGoto::getFullShortUrl, shortLinkUpdateReqDTO.getFullShortUrl())
            );
        }

        //删除缓存
        stringRedisTemplate.delete(REDIRECT_KEY + shortLinkUpdateReqDTO.getFullShortUrl());
        stringRedisTemplate.delete(REDIRECT_NULL_OBJECT_KEY + shortLinkUpdateReqDTO.getFullShortUrl());
    }

    @Override
    public IPage<ShortLinkPageRespDto> pageQuery(ShortLinkPageReqDto shortLinkPageReqDto) {

        //查询短链接基本信息
        IPage<ShortLink> pageResult = page(shortLinkPageReqDto, new LambdaQueryWrapper<ShortLink>()
                .eq(ShortLink::getGid, shortLinkPageReqDto.getGid())
                .eq(ShortLink::getDelFlag, 0)
                .eq(ShortLink::getEnableStatus, 0));

        List<String> fullShortUrlList = pageResult.getRecords().stream().map(ShortLink::getFullShortUrl).toList();
        //查询total ,today 的uv,pv,uip
        List<LinkAccessStats> totalUvPvUipList = linkAccessStatsMapper
                .listUvPvUip(shortLinkPageReqDto.getGid(), fullShortUrlList, false);
        List<LinkAccessStats> todayUvPvUipList = linkAccessStatsMapper
                .listUvPvUip(shortLinkPageReqDto.getGid(), fullShortUrlList, true);

        //填充数据
        Map<String, LinkAccessStats> totalmap = new HashMap<>();
        totalUvPvUipList.forEach(each -> totalmap.put(each.getFullShortUrl(), each));
        Map<String, LinkAccessStats> todaymap = new HashMap<>();
        todayUvPvUipList.forEach(each -> todaymap.put(each.getFullShortUrl(), each));

        IPage<ShortLinkPageRespDto> page = pageResult.convert(
                shortLink -> {
                    ShortLinkPageRespDto shortLinkPageRespDto = BeanUtil.copyProperties(shortLink, ShortLinkPageRespDto.class);
                    String fullShortUrl = shortLinkPageRespDto.getFullShortUrl();

                    LinkAccessStats totalStats = totalmap.get(fullShortUrl);

                    shortLinkPageRespDto.setTotalPv(totalStats == null ? 0 : totalStats.getPv());
                    shortLinkPageRespDto.setTotalUv(totalStats == null ? 0 : totalStats.getUv());
                    shortLinkPageRespDto.setTotalUip(totalStats == null ? 0 : totalStats.getUip());

                    LinkAccessStats todayStats = totalmap.get(fullShortUrl);
                    shortLinkPageRespDto.setTodayPv(todayStats == null ? 0 : todayStats.getPv());
                    shortLinkPageRespDto.setTodayUv(todayStats == null ? 0 : todayStats.getUv());
                    shortLinkPageRespDto.setTodayUip(todayStats == null ? 0 : todayStats.getUip());
                    return shortLinkPageRespDto;
                });

        //排序
        sortPageResult(page, shortLinkPageReqDto.getOrderTag());
        return page;
    }

    private void sortPageResult(IPage<ShortLinkPageRespDto> page, String orderTag) {
        page.getRecords().sort((o1, o2) -> {
            if (orderTag == null) {
                // 如果 orderTag 为 null，默认按创建时间排序
                return o2.getCreateTime().compareTo(o1.getCreateTime());
            }
            // 根据 orderTag 的值进行排序
            return switch (orderTag) {
                case "totalPv" -> Integer.compare(o2.getTotalPv(), o1.getTotalPv());
                case "totalUv" -> Integer.compare(o2.getTotalUv(), o1.getTotalUv());
                case "totalUip" -> Integer.compare(o2.getTotalUip(), o1.getTotalUip());
                case "todayPv" -> Integer.compare(o2.getTodayPv(), o1.getTodayPv());
                case "todayUv" -> Integer.compare(o2.getTodayUv(), o1.getTodayUv());
                case "todayUip" -> Integer.compare(o2.getTodayUip(), o1.getTodayUip());
                default -> o2.getCreateTime().compareTo(o1.getCreateTime()); // 默认按创建时间排序
            };
        });
    }

    @Override
    public List<ShortLinkGroupCountQueryRespDto> listGroupShortLinkCount(List<String> gids) {
        return baseMapper.listCountByGid(gids);
    }

    @Override
    public void redirectUrl(String shortUri, HttpServletRequest request, HttpServletResponse response) {

        String fullShortUrl = createShortLinkDefaultDomain + "/" + shortUri;
        String dataKey = REDIRECT_KEY + fullShortUrl;
        String originUrl = stringRedisTemplate.opsForValue().get(dataKey);

        //缓存中有，直接跳转
        if (StrUtil.isNotBlank(originUrl)) {
            shortLinkStats(request, response, fullShortUrl);
            gotoUrl(response, originUrl);
            return;
        }

        boolean contains = shortLinkCreateCachePenetrationBloomFilter.contains(fullShortUrl);
        if (!contains) {
            gotoUrl(response, "/page/notfound");
            return;
        }

        String isNull = stringRedisTemplate.opsForValue().get(REDIRECT_NULL_OBJECT_KEY + fullShortUrl);
        if (Objects.equals(isNull, "-")) {
            gotoUrl(response, "/page/notfound");
            return;
        }
        RLock lock = redissonClient.getLock(LOCK_REDIRECT_KEY + fullShortUrl);

        lock.lock();

        try {
            //双重判断，保证数据只被更新一次
            //缓存中有，直接跳转
            originUrl = stringRedisTemplate.opsForValue().get(dataKey);
            if (StrUtil.isNotBlank(originUrl)) {
                shortLinkStats(request, response, fullShortUrl);
                gotoUrl(response, originUrl);
                return;
            }
            isNull = stringRedisTemplate.opsForValue().get(REDIRECT_NULL_OBJECT_KEY + fullShortUrl);
            if (Objects.equals(isNull, "-")) {
                gotoUrl(response, "/page/notfound");
                return;
            }
            ShortLinkGoto shortLinkGoto = shortLinkGotoMapper.selectOne(new LambdaQueryWrapper<ShortLinkGoto>()
                    .eq(ShortLinkGoto::getFullShortUrl, fullShortUrl)
            );

            if (shortLinkGoto == null) {//布隆过滤器出现误判，缓存空对象
                stringRedisTemplate.opsForValue().set(REDIRECT_NULL_OBJECT_KEY + fullShortUrl, "-", REDIRECT_NULL_OBJECT_TTL, TimeUnit.SECONDS);
                gotoUrl(response, "/page/notfound");
                return;
            }

            ShortLink originLink = lambdaQuery()
                    .eq(ShortLink::getDelFlag, 0)
                    .eq(ShortLink::getEnableStatus, 0)
                    .eq(ShortLink::getGid, shortLinkGoto.getGid())
                    .eq(ShortLink::getFullShortUrl, fullShortUrl)
                    .one();

            if (originLink == null || originLink.getValidDate() != null && originLink.getValidDate().before(new Date())) {
                //短链接有效期过期了
                stringRedisTemplate.opsForValue().set(REDIRECT_NULL_OBJECT_KEY + fullShortUrl, "-", REDIRECT_NULL_OBJECT_TTL, TimeUnit.SECONDS);
                gotoUrl(response, "/page/notfound");
                return;
            }

            stringRedisTemplate.opsForValue()
                    .set(dataKey, originLink.getOriginUrl(),
                            LinkUtil.getLinkCacheValidTime(originLink.getValidDate()), TimeUnit.MILLISECONDS);

            shortLinkStats(request, response, fullShortUrl);
            gotoUrl(response, originLink.getOriginUrl());
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }

    }

    private void gotoUrl(HttpServletResponse response, String url) {
        try {
            response.sendRedirect(url);
        } catch (IOException e) {
            log.error("重定向失败");
        }
    }

    /**
     * 组装短链接统计数据
     *
     * @return
     */
    private StatsMessageDto buildStatsMessageDto(HttpServletRequest request, HttpServletResponse response, String fullShortUrl) {
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        Cookie[] cookies = request.getCookies();
        AtomicReference<String> uv = new AtomicReference<>();
        String ip = LinkUtil.getIp(request);

        //添加第一次访问添加setCookie，后面有cookie就不计数
        Runnable addResponseCookieTask = () -> {
            uv.set(UUID.fastUUID().toString());

            Cookie uvCookie = new Cookie("uv", uv.get());
            uvCookie.setMaxAge(60 * 60 * 24 * 30);
            uvCookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.indexOf("/", 7), fullShortUrl.length()));
            response.addCookie(uvCookie);
            uvFirstFlag.set(Boolean.TRUE);

            boolean isFirst = Boolean.FALSE.equals(stringRedisTemplate.hasKey(UV_KEY + fullShortUrl));
            stringRedisTemplate.opsForSet().add(UV_KEY + fullShortUrl, uv.get());
            if (isFirst) {
                stringRedisTemplate.expire(UV_KEY + fullShortUrl, LinkUtil.getNextHourDifference(), TimeUnit.MILLISECONDS);
            }
        };
        if (ArrayUtil.isNotEmpty(cookies)) {
            Arrays.stream(cookies)
                    .filter(each -> Objects.equals(each.getName(), "uv"))
                    .findFirst()
                    .map(Cookie::getValue)
                    .ifPresentOrElse(each -> {
                        uv.set(each);
                        Long addUv = stringRedisTemplate.opsForSet().add(UV_KEY + fullShortUrl, each);
                        uvFirstFlag.set(addUv != null && addUv > 0L);
                    }, addResponseCookieTask);
        } else {
            addResponseCookieTask.run();
        }
        boolean isFirst = Boolean.FALSE.equals(stringRedisTemplate.hasKey(UIP_KEY + fullShortUrl));
        Long addIp = stringRedisTemplate.opsForSet().add(UIP_KEY + fullShortUrl, ip);
        if (isFirst) {
            stringRedisTemplate.expire(UIP_KEY + fullShortUrl, LinkUtil.getNextHourDifference(), TimeUnit.MILLISECONDS);
        }

        ClientMsg clientMsg = LinkUtil.getClientMsg(request);
        return StatsMessageDto.builder()
                .ip(ip)
                .uv(uv.get())
                .browser(clientMsg.getBrowserType())
                .device(clientMsg.getDeviceType())
                .network(clientMsg.getNetworkType())
                .os(clientMsg.getOs())
                .fullShortUrl(fullShortUrl)
                .uvFirstFlag(uvFirstFlag.get())
                .uipFirstFlag(addIp != null && addIp > 0L)
                .build();
    }


    public void shortLinkStats(HttpServletRequest request, HttpServletResponse response, String fullShortUrl) {
        shortLinkStatsProducer.SendShortLinkStatsMessage(buildStatsMessageDto(request, response, fullShortUrl));
    }

    private String generateSuffix() {
        String generateRes;
        int generateCount = 0;
        while (true) {
            if (generateCount > 10) {
                throw new ServiceException("短链接频繁生成，请稍后再试");
            }
            //加上uuid
            //String originUrl = shortLinkCreateReqDTO.getOriginUrl() + UUID.randomUUID().toString(true);
            generateRes = RandomGenerator.generateRandom(6);

            if (!shortLinkCreateCachePenetrationBloomFilter.contains(createShortLinkDefaultDomain + "/" + generateRes)) {
                break;
            }
            generateCount++;
        }
        return generateRes;
    }
}
